/*
	jGrouse framework
	Copyright 2007 by Denis Riabtchik 
	All rights reserved.
	
	Licensed under modified BSD license. See license.txt for details
*/


/**
	Module with stuff required for MVC framework
	@module jgrouse.util.MvcDriver
	@requires jgrouse.util.PropertyChangeSupport
*/
jgrouse.module
({
name : 'jgrouse.util.MvcDriver',
requires : ['jgrouse.util.PropertyChangeSupport'],
imports : ['jgrouse.util.PropertyChangeSupport', 
	'jgrouse.util.PropertyChangeEvent', 'jgrouse.util.Catalog'],
body : function(embed)
{
	eval(embed);
	

	

	var ListenerAdaptor = function(listener, property)
	{
		this._listener = listener;
		this._property = property;
		return this;
	}
	
	ListenerAdaptor.prototype = 
	{
		equals : function(obj2)
		{
			if (!obj2)
			{
				return false;
			}
			return (this === obj2) || (this._listener === obj2._listener && this._property === obj2._property)
		}
	}
	
	/**
		Helper class that is used internally by MVC framework
		@class jgrouse.util.MvcPropertyChangeSupport
		@extends jgrouse.util.PropertyChangeSupport
	*/
	jgrouse.define('jgrouse.util.MvcPropertyChangeSupport', 'jgrouse.util.PropertyChangeSupport', function(salt)
	{
		eval(salt);
		
		return {
			/**
				@constructor initialize
			*/
			initialize : function(owner)
			{
				_super(this, 'initialize', owner);
			},
			
			/**
				@function triggerListener
				@inheritdesc
			*/
			triggerListener : function(listener, event)
			{
				var ownerEvent = this._owner._currentEvent;
				if (!ownerEvent || (ownerEvent.getSender() != listener._listener) ||  (ownerEvent.getPropertyName() != listener._property))
				{
					var newEvent = new PropertyChangeEvent(event.getSender(), listener._property, event.getNewValue(), event.getOldValue());
					_super(this, 'triggerListener', listener._listener, newEvent);
				}
			},
			
			/**
				Returns handler method for given property name. 
				Similar to {@link jgrouse.util.PropertyChangeSupport.getListenerMethod} but it checks first
				for method called <code>set&lt;PropName&gt;</code>
				@function getListenerMethod
				@param {jgrouse.util.PropertyChangeListener} listener - listener to be triggered
				@param {String} property being changed
			*/
			getListenerMethod : function(listener, propName)
			{
				var setter = 'set' + jgrouse.string.upperFirst(propName);
				if (listener[setter])
				{
					return setter;
				}
				return _super(this, 'getListenerMethod', listener, propName);
			}
		};
	});
	
	/**
		MVC driver is the class that acts as a dispatcher and glues together model, view and controller.
		This class allows binding of view properties to model properties via controller.
		Whenever a property of a view is changed, appropriate methods of controller are triggered.
		Controller would perform necessary business logic and change properties of model. Those changes
		are detected by the framework and MvcDriver would notify views that are bound to changed model's 
		properties.
		@class jgrouse.util.MvcDriver
		@extends jgrouse.BaseObject
	*/
	jgrouse.define('jgrouse.util.MvcDriver', 'jgrouse.BaseObject', function(salt)
	{
		eval(salt);
		
		return {
			/**
				@constructor initialize
			*/
			initialize : function(controller)
			{
				_super(this, 'initialize');
				this._viewPCS = new MvcPropertyChangeSupport(this);
				this._modelPCS = new Catalog(this._viewPropComparator);
				this._controller = controller;
				this._clean = null;
				this._dirty = null;
				this._pending = [];
				this._lock = 0;
			},
			
			_viewPropComparator : function(key, item)
			{
				var k = item.key;
				return key === k || (key.view === k.view && key.property === k.property);
			},
			
			/**
				Binds a property of view to property of model.
				@function bind
				@param {jgrouse.util.PropertyChangeListener} view 
				@param {String} viewProperty - name of property published by view
				@param {String} modelProperty - name of model's property 
			*/
			bind : function(view, viewProperty, modelProperty)
			{
				view.addListener(viewProperty, this);				
				this._viewPCS.addListener(modelProperty, new ListenerAdaptor(view, viewProperty));
				this._modelPCS.add({view:view, property:viewProperty}, modelProperty);
			},
			
			/**
				Unbinds view's property from model. All values should be the same as passed to {@link bind}
				@function unbind
				@param {jgrouse.util.PropertyChangeListener} view 
				@param {String} viewProperty - name of property published by view
				@param {String} modelProperty - name of model's property 
			*/
			unbind : function(view, viewProperty, modelProperty)
			{
				view.removeListener(viewProperty, this);
				this._viewPCS.removeListener(modelProperty, new ListenerAdaptor(view, viewProperty));
				this._modelPCS.remove({view:view, property:viewProperty}, modelProperty);
			},
			
			/**
				Unbind all view's properties from model
				@function unbindView
				@param {jgrouse.util.PropertyChangeListener} view
			*/
			unbindView : function(view)
			{
				view.removeListenerFull(view);
				var boxKeys = this._modelPCS.keys();
				var athis = this;
				jgrouse.forEach(boxKeys, function(boxKey, index)
				{
					if (boxKey.view === view)
					{
						athis._modelPCS.removeBox(boxKey);
					}
				});
				//this._modelPCS.removeAll(viewProperty);
				var mappings = [];
				this._viewPCS.forEach(function(listener, property)
				{
					if (listener._listener === view)
					{
						mappings.push({listener : listener, modelProperty : property});
					}
				});
				for (var i = 0; i < mappings.length; i++)
				{
					var m = mappings[i];
					this._viewPCS.removeListener(m.modelProperty, m.listener);
				}
			},
			
			getHandlerName : function(propName)
			{
				return 'onPropertyChanged';
			},
			
			_processPending : function()
			{
				if (this._pending.length > 0)
				{
					var e1 = this._pending[0];
					this._pending.splice(0, 1);
					this.onPropertyChanged(e1);
				}
			},
			
			onPropertyChanged : function(event)
			{
				if (this._lock === 0)
				{
					this._currentEvent = event;
					try
					{
						this._internalPropertyChange(event);
					}
					finally
					{
						this._currentEvent = null;
					}	
					this._processPending();
				}
				else
				{
					this._pending.push(event);
				}
			},
			
			_setModelProperty : function(modelProperty, newValue, oldValue)
			{
				var methodName = null;
				var us = jgrouse.string.upperFirst(modelProperty);
				if (modelProperty.indexOf('trigger') === 0)
				{
					methodName = 'on' + us;
				}
				else
				{
					methodName = 'set' + us;
				}
				if (this._controller[methodName])
				{
					this._controller[methodName](newValue, oldValue);
				}
				else
				{
					this._dirty[modelProperty] = newValue;
				}
			},
			
			_flush : function()
			{
				this._lock++;
				try
				{
					jgrouse.syncData(this._clean, this._dirty, this);
				}
				finally
				{
					this._lock--;
				}
			},
			
			_internalPropertyChange : function(event)
			{
				var athis = this;
				var newValue = event.getNewValue();
				var oldValue = event.getOldValue();
				var key = {view : event.getSender(), property: event.getPropertyName()};
				
				this._modelPCS.forEachInBox(key, function(modelProperty, index)
				{
					athis._setModelProperty(modelProperty, newValue, oldValue);
				});
				this._flush();
			},

			/**
				Sets model values
				@function setData
				@param {Object} data - object that should be used as model
			*/			
			setData : function(data)
			{
				this._clean = {};
				this._dirty = {};
				jgrouse.copyData(this._dirty, data);
				this._controller.setData(this._dirty);
				this._flush();
				this._processPending();
			},
			
			onWatch : function(data, modelPropertyName, newValue, oldValue)
			{
				this._viewPCS.firePropertyChange(modelPropertyName, newValue, oldValue);
				if (modelPropertyName.indexOf('trigger') === 0)
				{
					delete this._dirty[modelPropertyName];
					delete this._clean[modelPropertyName];
				}
			},
			
			/**
				Force property change handling
				@function setProperty
				@param {String} property - name of property
				@param {Object} value - new value of property
			*/
			setProperty : function(property, value)
			{
				this._setModelProperty(property, value);
				this._flush();
			}
		}
	});
	
	/**
		Base class for controllers in MVC framework
		@class jgrouse.util.BaseController
		@extends jgrouse.BaseObject
	*/
	jgrouse.define('jgrouse.util.BaseController', 'jgrouse.BaseObject', function(embed)
	{
		eval(embed);
		
		return {
			/**
				Initialize controller. If no data passed to controller, it would use empty object.
				@constructor initialize
				@param {Object} data - initial data to be used by controller, optional
			*/
			initialize : function(data)
			{
				_sup(this, arguments);
				this.setData(data? data : {});
			},
			
			/**
				Set data for controller
				@function setData
				@param {Object} data
			*/
			setData : function(data)
			{
				this._data = data;
			},
			
			/**
				Retrieve current data from controller
				@function {Object} getData
			*/
			getData : function()
			{
				return this._data;
			}
		}
	});
}
	
})